import subprocess
from channels.generic.websocket import WebsocketConsumer
from django.conf import settings
from hippo_api.utils.handle_key import AppSetting
from host.models import Host
from threading import Thread
import json
from host.models import PkeyModel
from release.models import ReleaseApply

# 继承WebsocketConsumer
class SSHConsumer(WebsocketConsumer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 获取从前端客户端 传来的主机ID   path('ws/ssh/<int:id>/'...这里的ID
        self.id = self.scope['url_route']['kwargs']['id']
        print('>>>>id', self.id)
        # print(self.scope['url_route'])
        self.chan = None
        self.ssh = None

    # 3 接受连接返回结果并返回给客户端
    # 5 接受主机指令的执行结果，并返回给客户端
    def loop_read(self):
        while True:
            data = self.chan.recv(32 * 1024)  # self.chan伪终端 没有指令就一直等待
            if not data:
                self.close(3333)
                break
            self.send(data.decode())  # 发送返回结果给前端客户端

    # 4 接收客户端发送过来的指令，并发送给主机执行指令
    def receive(self, text_data=None, bytes_data=None):
        data = text_data or bytes_data
        print('receive:  xxxxxx', data, type(data))  # 打印的是前端客户端发送的指令

        # 因为这里获取前端发来的命令, 所以在这里就可以做指令的过滤检查，堡垒机功能

        if data:
            self.chan.send(data + '\r\n')  # 向主机连接的伪终端，发送指令 \r\n就是回车，才能执行

    def disconnect(self, code):
        self.chan.close()
        self.ssh.close()
        print('Connection close')

    # 1 请求来了自动触发父类connect方法，我们继承拓展父类的connect方法，因为我们和客户端建立连接的同时，就可以和客户端想要操作的主机建立一个ssh连接通道。
    def connect(self):
        print('connect连接来啦74')
        self.accept()  # 建立websocket连接  接受连接，建立连接, 浏览器客户端和后端服务的建立好websocket了
        self._init()  # 建立和主机的ssh连接  这是自己定义的, 用于后端服务和 服务器建立链接

    # 2 建立和主机的ssh连接
    def _init(self):
        # self.send(bytes_data=b'Connecting ...\r\n')
        self.send('Connecting ...\r\n')  # 通过服务端直接给前端客户端websocket建立的链接通道，发了消息给客户端
        # 从数据库查询对应主机
        host = Host.objects.filter(pk=self.id).first()
        if not host:
            # 如果没找到对应主机，返回消息
            self.send(text_data='Unknown host\r\n')
            # 关闭链接
            self.close()
        try:
            # 从数据库获取私钥
            pkey = PkeyModel.objects.get(key='private_key').value
            # 通过ssh工具类 通过私钥,建立ssh连接
            self.ssh = host.get_ssh(pkey).get_client()
        except Exception as e:
            self.send(f'Exception: {e}\r\n')
            self.close()
            return

        # invoke_shell长链接和直接执行exec命令不同，他是一段一段的
        # invoke_shell激活shell终端模式，也就是长连接模式，exec_command()函数是将服务器执行完的结果一次性返回给你；invoke_shell()函数类似shell终端，可以将执行结果分批次返回，所以我们接受数据时需要循环的取数据
        # 激活伪终端模式
        self.chan = self.ssh.invoke_shell(term='xterm')
        # 连接中没有任何信息时，该连接能够维持30秒
        self.chan.transport.set_keepalive(30)

        # 不能阻塞了主程序, 所以单独起一个线程
        # 和主机的连接一旦建立，主机就会将连接信息返回给服务端和主机的连接通道中，并且以后我们还要在这个通道中进行指令发送和指令结果的读取，所以我们开启单独的线程，去连接中一直等待和获取指令执行结果的返回数据
        Thread(target=self.loop_read).start()


# 执行代码发布 推送到目标主机
def put_code_to_target(host, cli, cmd_list, self):
    """
    :param host: 所有要部署的主机
    :param cli: ssh的客户端
    :param cmd: 执行的明显
    :param self: websocket本身,用于send发送消息给前端
    :return:
    """
    for cmd in cmd_list:
        self.send(cmd.get('msg'))
        if cmd.get('step') == '5':
            try:
                cli.put_file(cmd.get('local_path'), cmd.get('remote_path'),)
            except Exception as e:
                error_msg = {
                    'step': cmd.get('step'),  # 表示自动化部署进行到第几步了
                    'error': str(e),  # 错误信息
                    'ip_addr': host.ip_addr,  # 错误的主机
                    'hostname': host.hostname,
                    'status': 1,  # status不等于0，表示有错误
                }
                error_msg_str = json.dumps(error_msg)
                self.send(error_msg_str)
                self.close()
                break
        code, result_content = cli.exec_command(cmd.get('cmd'))
        if code != 0:
            error_msg = {
                'step': cmd.get('step'),  # 表示自动化部署进行到第几步了
                'error': result_content, # 错误信息
                'ip_addr': host.ip_addr,  # 错误的主机
                'hostname': host.hostname,
                'status': 1,  # status不等于0，表示有错误
            }
            error_msg_str = json.dumps(error_msg)
            self.send(error_msg_str)
            self.close()
            break

# 发布过程记录的类
class ReleaseConsumer(WebsocketConsumer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # self.user = self.scope['user']
        # print('SSHConsumer>>>41')
        self.id = self.scope['url_route']['kwargs']['id']
        print('>>>>id', self.id)
        # print(self.scope['url_route'])
        self.chan = None
        self.ssh = None

    def connect(self):
        print('发布连接来啦！！！！')
        self.accept()
        self._init()

    def _init(self):
        self.send('Connecting ...\r\n')  # send向前端发数据
        # 查询本次发布的记录
        release_apply_obj = ReleaseApply.objects.filter(pk=self.id).first()
        release_record_obj = release_apply_obj.release_record
        # print(release_record_obj)

        # 1 检出(git clone)前的动作
        self.send('执行检出前的动作。。。')
        '''
            filefilterway   文件过滤方式
            file_filter_cmd_content  过滤的文件或目录
            before_code_check_out_value  代码检出前的指令(针对本机)
            before_release_content  代码发布前的指令(针对目标主机)
            custom_global_variable 部署运行的项目时给项目设置的全局变量
            after_code_check_out_value  代码检出后的指令(针对本机)
            after_release_value 代码发布后的指令(针对目标主机)
        '''
        code, output = subprocess.getstatusoutput(release_record_obj.before_code_check_out_value)
        # code, output = subprocess.getstatusoutput(release_record_obj.before_code_check_out_value)
        if code != 0:
            error_msg = {
                'step': 1,  # 表示自动化部署进行到第几步了
                'error': output, # 错误信息
                'status': 1,  # status不等于0，表示有错误
            }
            error_msg_str = json.dumps(error_msg)

            self.send(error_msg_str) # 发送错误信息
            self.close()  # 并关闭wbsocket通道
            # 并还原为执行动作之前的状态（这里我就不写了）思路:检出前把代码目录改名,如果此次失败,删除当前目录,并恢复改名的目录
            return code
        self.send('执行代码检出前动作完成。。。')

        # 2 检出代码
        self.send('执行代码检出中。。。')
        git_attr = release_record_obj.code_git_addr
        # 获取项目名称，方便后面创建文件夹和打包项目代码目录
        project_name = git_attr.rsplit('/')[-1].split('.')[0].strip()
        # print('project_name>>', project_name)
        code, output = subprocess.getstatusoutput(f'cd {settings.RELEASE_LOCAL_PATH} && git clone {git_attr}')

        # code, output = subprocess.getstatusoutput(f'git clone {release_record_obj.code_git_addr}')
        if code != 0:
            error_msg = {
                'step': 2,
                'error': output,
                'status': 1,
            }
            error_msg_str = json.dumps(error_msg)
            self.send(error_msg_str)# 发送错误信息
            self.close()  # 并关闭wbsocket通道
            # 并还原为执行动作之前的状态
        self.send('执行代码检出完成。。。')

        # 3 检出后动作
        self.send('执行代码检出后的动作。。。')
        code, output = subprocess.getstatusoutput(f'cd {settings.RELEASE_LOCAL_PATH} {project_name} && {release_record_obj.after_code_check_out_value} {project_name}.tar.gz {project_name}')
        # code, output = subprocess.getstatusoutput(release_record_obj.after_code_check_out_value)
        if code != 0:
            error_msg = {
                'step': 3,
                'error': output,
                'status': 1,
            }
            error_msg_str = json.dumps(error_msg)
            self.send(error_msg_str)  # 发送错误信息
            self.close()  # 并关闭wbsocket通道
            # 并还原为执行动作之前的状态
        self.send('执行代码检出后的动作完成。。。')
        # 如果检出没有问题的话，代码继续往下面执行
        # self.send('执行代码发布前动作。。。')
        # 后面3步是在点击发布的时候进行的

        # 4 发布前，对目标主机执行的动作
        release_record_detail_list = release_record_obj.release_record.all()  # 获取所有要部署的服务器
        # 根据本次发布查询到的主机列表
        target_host_list = []
        for record_detail in release_record_detail_list:
            target_host_list.append(record_detail.hosts)

        # 开启多线程或者线程池来并发执行多个主机的代码推送
        # 并且将推送的结果和错误保存到redis中，
        # 将代码发布前4  发布 5 发布后6 的指令作为成一个列表交给线程任务
        cmd_list = [
            {'step': '4','cmd': release_record_obj.before_release_content, 'msg': '代码发布前动作。。。', },
            {'step': '5','cmd': 'ls', 'msg': '代码发布中。。。', 'local_path': f'{settings.RELEASE_LOCAL_PATH}/{project_name}.tar.gz', 'remote_path': f'{release_record_obj.target_host_pub_path}/{project_name}.tar.gz'},
            {'step': '6','cmd': release_record_obj.after_release_value, 'msg': '代码发布后动作。。。', }
        ]

        # 所有要部署的服务器 建立ssh连接
        pkey = AppSetting.get('private_key')
        t_list = []
        for host_obj in target_host_list:
            cli = host_obj.get_ssh(pkey)
            t = Thread(target=put_code_to_target,args=(host_obj,cli,cmd_list, self))
            t.start()
            t_list.append(t)

        for tt in t_list:
            tt.join()
        # self.send(json.dumps(a))
        self.send('代码发布完成！！！！')

        # self.close()  # 并关闭wbsocket通道
        # 并还原为执行动作之前的状态